﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using CSharpExt.Util;
using log4net;

namespace CSharpExt.Xml
{

    /// <summary>
    /// node ::= 
    ///     path
    ///     path/text()
    ///     path/cdata()
    ///     path/@attribute
    /// 
    /// path ::= tag(/tag)*
    /// 
    /// tag ::= 
    ///     .
    ///     ..
    ///     name
    ///     name[index]  
    ///     name[{param}]
    ///     name[@attribute='value']    // SENZA SPAZI TRA '[' E '@'!
    ///     name[@attribute="value"]
    ///     name[@attribute={param}]
    ///     name[@attribute=value]      // GLI SPAZI IN TESTA ED IN CODA VENGONO RIMOSSI
    /// 
    /// Nota: index parte da 1!!!!
    /// </summary>
    public static class XPathUtils
    {
        #region Commons

        private static char[] SEPARATOR = { '/' };
        private static char[] SPACES = { '\r', '\n', '\t', ' ' };
        private static Dictionary<String, Object> NO_PARAMS = new Dictionary<String, Object>();



        private static int resolveIndex(String s, Dictionary<String, Object> parameters)
        {
            if (s.StartsWith("{"))
                s = parameters[s.Substring(1, s.Length - 2)].ToString();
            return int.Parse(s);
        }

        private static String resolveValue(String s, Dictionary<String, Object> parameters)
        {
            if (s.StartsWith("{"))
                s = parameters[s.Substring(1, s.Length - 2)].ToString();
            else if (s.StartsWith("'") || s.StartsWith("\""))
                s = s.Substring(1, s.Length - 2);
            else
                s = s.Trim();
            return s;
        }

        #endregion

        #region Create (XmlDocument)

        public static XmlElement CreateDocument(String nodeName)
        {
            XmlDocument doc = new XmlDocument();
            XmlElement root = doc.CreateElement(nodeName);
            doc.AppendChild(root);
            return root;
        }

        public static XmlElement CreateDocument(String nodeName, String nsPrefix, String nsURI)
        {
            XmlElement root = CreateDocument(nodeName);
            root.SetAttribute(nsPrefix, nsURI);

            return root;
        }

        public static XmlNode CreateNode(XmlNode node, String xpath)
        {
            return CreateNode(node, xpath, NO_PARAMS);
        }
        public static XmlNode CreateNode(XmlNode node, String xpath, Object p0)
        {
            Dictionary<String, Object> parameters = new Dictionary<String, object>();
            parameters["0"] = p0;
            return CreateNode(node, xpath, parameters);
        }
        public static XmlNode CreateNode(XmlNode node, String xpath, Object p0, Object p1)
        {
            Dictionary<String, Object> parameters = new Dictionary<String, object>();
            parameters["0"] = p0;
            parameters["1"] = p1;
            return CreateNode(node, xpath, parameters);
        }
        public static XmlNode CreateNode(XmlNode node, String xpath, Object p0, Object p1, Object p2)
        {
            Dictionary<String, Object> parameters = new Dictionary<String, object>();
            parameters["0"] = p0;
            parameters["1"] = p1;
            parameters["2"] = p2;
            return CreateNode(node, xpath, parameters);
        }

        public static XmlNode CreateNode(XmlNode node, String xpath, Dictionary<String, Object> parameters)
        {
            return selectNode(node, xpath, true, parameters);
        }

        #endregion

        #region Read (XmlDocument)

        public static String Read(XmlNode node, String xpath)
        {
            return Read(node, xpath, String.Empty, NO_PARAMS);
        }
        public static String Read(XmlNode node, String xpath, String defaultValue)
        {
            return Read(node, xpath, defaultValue, NO_PARAMS);
        }
        public static String Read(XmlNode node, String xpath, String defaultValue, Object p0)
        {
            Dictionary<String, Object> parameters = new Dictionary<String, Object>();
            parameters["0"] = p0;
            return Read(node, xpath, defaultValue, NO_PARAMS);
        }
        public static String Read(XmlNode node, String xpath, String defaultValue, Object p0, Object p1)
        {
            Dictionary<String, Object> parameters = new Dictionary<String, Object>();
            parameters["0"] = p0;
            parameters["1"] = p1;
            return Read(node, xpath, defaultValue, NO_PARAMS);
        }
        public static String Read(XmlNode node, String xpath, String defaultValue, Object p0, Object p1, Object p2)
        {
            Dictionary<String, Object> parameters = new Dictionary<String, Object>();
            parameters["0"] = p0;
            parameters["1"] = p1;
            parameters["2"] = p2;
            return Read(node, xpath, defaultValue, NO_PARAMS);
        }

        public static String Read(XmlNode node, String xpath, String defaultValue, Dictionary<String, Object> parameters)
        {
            XmlNode selected = selectNode(node, xpath, false, parameters);
            if (selected == null)
                return defaultValue;

            String value;
            if (selected.NodeType == XmlNodeType.Element)
                value = composeTextNodes(selected);
            else if (selected.NodeType == XmlNodeType.Text)
                value = composeTextNodes(selected.ParentNode);
            else
                value = selected.Value;

            return String.IsNullOrEmpty(value) ? defaultValue : value;
        }

        #endregion

        #region Write (XmlDocument)

        public static XmlNode Write(XmlNode node, String xpath, String value)
        {
            return Write(node, xpath, value, NO_PARAMS);
        }
        public static XmlNode Write(XmlNode node, String xpath, String value, Object p0)
        {
            Dictionary<String, Object> parameters = new Dictionary<String, Object>();
            parameters["0"] = p0;
            return Write(node, xpath, value, parameters);
        }
        public static XmlNode Write(XmlNode node, String xpath, String value, Object p0, Object p1)
        {
            Dictionary<String, Object> parameters = new Dictionary<String, Object>();
            parameters["0"] = p0;
            parameters["1"] = p1;
            return Write(node, xpath, value, parameters);
        }
        public static XmlNode Write(XmlNode node, String xpath, String value, Object p0, Object p1, Object p2)
        {
            Dictionary<String, Object> parameters = new Dictionary<String, Object>();
            parameters["0"] = p0;
            parameters["1"] = p1;
            parameters["2"] = p2;
            return Write(node, xpath, value, parameters);
        }

        public static XmlNode Write(XmlNode node, String xpath, String value, Dictionary<String, Object> parameters)
        {
            XmlNode selected = selectNode(node, xpath, true, parameters);
            if (selected.NodeType == XmlNodeType.Element)
            {
                removeTextNodes(selected);
                selected = createTextNode(selected);
            }
            {
                selected.Value = value;
            }
            return selected;
        }

        #endregion

        #region Implementation (XmlDocument)

        private static XmlNode selectNode(XmlNode node, String xpath, bool create, Dictionary<String, Object> parameters)
        {
            String[] steps = xpath.Split(SEPARATOR);
            foreach (String step in steps)
            {
                if (node == null)
                    break;
                else
                    node = selectStep(node, step, create, parameters);
            }
            return node;
        }

        private static XmlText createTextNode(XmlNode node)
        {
            XmlDocument doc = selectOwnerDocument(node);
            XmlText text = doc.CreateTextNode("");
            node.AppendChild(text);
            return text;
        }

        private static void removeTextNodes(XmlNode node)
        {
            foreach (XmlNode child in node.ChildNodes)
            {
                if (child.NodeType == XmlNodeType.Text)
                    node.RemoveChild(child);
            }
        }

        private static XmlCDataSection createCDataNode(XmlNode node)
        {
            XmlDocument doc = selectOwnerDocument(node);
            XmlCDataSection cdata = doc.CreateCDataSection("");
            node.AppendChild(cdata);
            return cdata;
        }

        private static void removeCDataNodes(XmlNode node)
        {
            foreach (XmlNode child in node.ChildNodes)
            {
                if (child.NodeType == XmlNodeType.CDATA)
                    node.RemoveChild(child);
            }
        }

        private static String composeTextNodes(XmlNode node)
        {
            StringBuilder sb = new StringBuilder();
            foreach (XmlNode child in node.ChildNodes)
            {
                if (child.NodeType == XmlNodeType.Text)
                    sb.Append(child.Value);
            }
            return sb.ToString().Trim(SPACES);
        }

        private static XmlNode selectStep(XmlNode node, String step, bool create, Dictionary<String, Object> parameters)
        {
            if (step.Length == 0)               // ""
                return selectOwnerDocument(node);
            if (".".Equals(step))               // "."
                return node;
            if ("..".Equals(step))              // ".."
                return node.ParentNode;
            if ("text()".Equals(step))          // "text()"
                return selectTextNode(node, create);
            if ("cdata()".Equals(step))          // "text()"
                return selectCDataNode(node, create);
            if (step.StartsWith("@"))           // "@attribute"
                return selectAttributeNode(node, step, create);
            if (step.IndexOf("[@") != -1)       // "n[@a='v'] | n[@a="v"] | n[@a={p}]
                return selectConditionNode(node, step, create, parameters);
            if (step.IndexOf("[") != -1)        // "n[i]" | n[{p}]
                return selectIndexedNode(node, step, create, parameters);
            else                                // n
                return selectNamedNode(node, step, create);
        }

        private static XmlDocument selectOwnerDocument(XmlNode node)
        {
            return node.NodeType == XmlNodeType.Document ? (XmlDocument)node : node.OwnerDocument;
        }

        private static XmlNode selectTextNode(XmlNode node, bool create)
        {
            if (!create)
                return node;

            removeTextNodes(node);
            return createTextNode(node);
        }

        private static XmlNode selectCDataNode(XmlNode node, bool create)
        {
            if (!create)
                return node;

            removeCDataNodes(node);
            return createCDataNode(node);
        }


        private static XmlNode selectAttributeNode(XmlNode node, String step, bool create)
        {
            String name = step.Substring(1);
            XmlElement element = (XmlElement)node;
            foreach (XmlAttribute attribute in element.Attributes)
            {
                if (name.Equals(attribute.Name))
                    return attribute;
            }

            if (!create)
                return null;

            return createAttributeNode(element, name);
        }

        private static XmlNode selectConditionNode(XmlNode node, String step, bool create, Dictionary<String, Object> parameters)
        {
            String nodeName, attribute, value;

            // name[@attribute='value']
            // name[@attribute="value"]
            // name[@attribute={param}]
            // name[@attribute=value]
            {
                int pos = step.IndexOf('[');
                nodeName = step.Substring(0, pos);

                String condition = step.Substring(pos + 1, step.Length - pos - 2);
                int peq = condition.IndexOf('=');

                attribute = condition.Substring(1, peq - 1).Trim();

                String quoted = condition.Substring(peq + 1).Trim();
                value = resolveValue(quoted, parameters);
            }

            foreach (XmlNode child in node.ChildNodes)
            {
                if (!nodeName.Equals(child.Name))
                    continue;

                if (((XmlElement)child).GetAttribute(attribute).Equals(value))
                    return child;
            }

            if (!create)
                return null;

            {
                XmlDocument doc = selectOwnerDocument(node);
                XmlElement newChild = doc.CreateElement(nodeName);
                newChild.SetAttribute(attribute, value);
                node.AppendChild(newChild);
                return newChild;
            }
        }

        private static XmlNode selectIndexedNode(XmlNode node, String step, bool create, Dictionary<String, Object> parameters)
        {
            int pos = step.IndexOf('[');
            String nodeName = step.Substring(0, pos);
            String index = step.Substring(pos + 1, step.Length - pos - 2);
            int at = resolveIndex(index, parameters);

            foreach (XmlNode child in node.ChildNodes)
            {
                if (nodeName.Equals(child.Name))
                {
                    if (at == 1)
                        return child;

                    --at;
                }
            }

            if (!create)
                return null;

            // deve creare abbastanza child 
            XmlDocument doc = selectOwnerDocument(node);
            XmlElement newChild = null;
            while (at > 0)
            {
                newChild = doc.CreateElement(nodeName);
                node.AppendChild(newChild);
                --at;
            }

            return newChild;
        }

        private static XmlNode selectNamedNode(XmlNode node, String nodeName, bool create)
        {
            foreach (XmlNode child in node.ChildNodes)
            {
                if (nodeName.Equals(child.Name))
                    return child;
            }

            if (!create)
                return null;

            XmlDocument doc = selectOwnerDocument(node);
            XmlElement newChild = doc.CreateElement(nodeName);
            node.AppendChild(newChild);
            return newChild;
        }


        private static XmlAttribute createAttributeNode(XmlElement element, String name)
        {
            XmlDocument doc = selectOwnerDocument(element);
            XmlAttribute newAttribute = doc.CreateAttribute(name);
            element.SetAttributeNode(newAttribute);
            return newAttribute;
        }

        #endregion

        // ==================================================================

        #region Create (XDocument)

        public static XElement XCreateDocument(String nodeName)
        {
            XDocument doc = new XDocument();
            XElement root = new XElement(nodeName);
            doc.Add(root);
            return root;
        }

        public static XElement XCreateDocument(String nodeName, String nsPrefix, String nsURI)
        {
            XElement root = XCreateDocument(nodeName);
            root.Add(new XAttribute(nsPrefix, nsURI));

            return root;
        }

        public static XNode CreateNode(XNode node, String xpath)
        {
            return CreateNode(node, xpath, NO_PARAMS);
        }
        public static XNode CreateNode(XNode node, String xpath, Object p0)
        {
            Dictionary<String, Object> parameters = new Dictionary<String, object>();
            parameters["0"] = p0;
            return CreateNode(node, xpath, parameters);
        }
        public static XNode CreateNode(XNode node, String xpath, Object p0, Object p1)
        {
            Dictionary<String, Object> parameters = new Dictionary<String, object>();
            parameters["0"] = p0;
            parameters["1"] = p1;
            return CreateNode(node, xpath, parameters);
        }
        public static XNode CreateNode(XNode node, String xpath, Object p0, Object p1, Object p2)
        {
            Dictionary<String, Object> parameters = new Dictionary<String, object>();
            parameters["0"] = p0;
            parameters["1"] = p1;
            parameters["2"] = p2;
            return CreateNode(node, xpath, parameters);
        }

        public static XNode CreateNode(XNode node, String xpath, Dictionary<String, Object> parameters)
        {
            return (XNode)selectNode(node, xpath, true, parameters);
        }

        #endregion

        #region Read (XDocument)

        public static String Read(XNode node, String xpath)
        {
            return Read(node, xpath, String.Empty, NO_PARAMS);
        }
        public static String Read(XNode node, String xpath, String defaultValue)
        {
            return Read(node, xpath, defaultValue, NO_PARAMS);
        }
        public static String Read(XNode node, String xpath, String defaultValue, Object p0)
        {
            Dictionary<String, Object> parameters = new Dictionary<String, Object>();
            parameters["0"] = p0;
            return Read(node, xpath, defaultValue, NO_PARAMS);
        }
        public static String Read(XNode node, String xpath, String defaultValue, Object p0, Object p1)
        {
            Dictionary<String, Object> parameters = new Dictionary<String, Object>();
            parameters["0"] = p0;
            parameters["1"] = p1;
            return Read(node, xpath, defaultValue, NO_PARAMS);
        }
        public static String Read(XNode node, String xpath, String defaultValue, Object p0, Object p1, Object p2)
        {
            Dictionary<String, Object> parameters = new Dictionary<String, Object>();
            parameters["0"] = p0;
            parameters["1"] = p1;
            parameters["2"] = p2;
            return Read(node, xpath, defaultValue, NO_PARAMS);
        }

        public static String Read(XNode node, String xpath, String defaultValue, Dictionary<String, Object> parameters)
        {
            XObject selected = selectNode(node, xpath, false, parameters);
            if (selected == null)
                return defaultValue;

            String value;
            if (selected.NodeType == XmlNodeType.Element)
                value = composeTextNodes((XElement)selected);
            else if (selected.NodeType == XmlNodeType.Text)
                value = composeTextNodes(selected.Parent);
            else if (selected.NodeType == XmlNodeType.CDATA)
                value = composeCDataNodes(selected.Parent);
            else if (selected.NodeType == XmlNodeType.Attribute)
                value = ((XAttribute)selected).Value;
            else
                value = ((XElement)selected).Value;

            return String.IsNullOrEmpty(value) ? defaultValue : value;
        }

        #endregion

        #region Write (XDocument)

        public static XObject Write(XObject node, String xpath, String value)
        {
            return Write(node, xpath, value, NO_PARAMS);
        }
        public static XObject Write(XObject node, String xpath, String value, Object p0)
        {
            Dictionary<String, Object> parameters = new Dictionary<String, Object>();
            parameters["0"] = p0;
            return Write(node, xpath, value, parameters);
        }
        public static XObject Write(XObject node, String xpath, String value, Object p0, Object p1)
        {
            Dictionary<String, Object> parameters = new Dictionary<String, Object>();
            parameters["0"] = p0;
            parameters["1"] = p1;
            return Write(node, xpath, value, parameters);
        }
        public static XObject Write(XObject node, String xpath, String value, Object p0, Object p1, Object p2)
        {
            Dictionary<String, Object> parameters = new Dictionary<String, Object>();
            parameters["0"] = p0;
            parameters["1"] = p1;
            parameters["2"] = p2;
            return Write(node, xpath, value, parameters);
        }

        public static XObject Write(XObject node, String xpath, String value, Dictionary<String, Object> parameters)
        {
            XObject selected = selectNode(node, xpath, true, parameters);

            switch (selected.NodeType)
            {
                case XmlNodeType.Element:
                    removeTextNodes((XElement)selected);
                    selected = createTextNode((XElement)selected);
                    ((XText)selected).Value = value;
                    break;

                case XmlNodeType.Text:
                    selected = selected.Parent;
                    removeTextNodes((XElement)selected);
                    selected = createTextNode((XElement)selected);
                    ((XText)selected).Value = value;
                    break;

                case XmlNodeType.CDATA:
                    selected = selected.Parent;
                    removeCDataNodes((XElement)selected);
                    selected = createCDataNode((XElement)selected);
                    ((XCData)selected).Value = value;
                    break;

                case XmlNodeType.Attribute:
                    ((XAttribute)selected).Value = value;
                    break;
            }

            return selected;
        }

        #endregion

        #region Implementation (XDocument)

        private static XObject selectNode(XObject node, String xpath, bool create, Dictionary<String, Object> parameters)
        {
            String[] steps = xpath.Split(SEPARATOR);
            foreach (String step in steps)
            {
                if (node == null)
                    break;
                else
                    node = selectStep(node, step, create, parameters);
            }
            return node;
        }

        private static XText createTextNode(XElement node)
        {
            XText text = new XText("");
            node.Add(text);
            return text;
        }

        private static void removeTextNodes(XElement node)
        {
            foreach (XNode child in new List<XNode>(node.Nodes()))
            {
                if (child.NodeType == XmlNodeType.Text)
                    child.Remove();
            }
        }

        private static XCData createCDataNode(XElement node)
        {
            XCData cdata = new XCData("");
            node.Add(cdata);
            return cdata;
        }

        private static void removeCDataNodes(XElement node)
        {
            foreach (XNode child in new List<XNode>(node.Nodes()))
            {
                if (child.NodeType == XmlNodeType.CDATA)
                    child.Remove();
            }
        }

        private static String composeTextNodes(XElement node)
        {
            StringBuilder sb = new StringBuilder();
            foreach (XNode child in node.Nodes())
            {
                if (child.NodeType == XmlNodeType.Text)
                    sb.Append(((XText)child).Value);
            }
            return sb.ToString().Trim(SPACES);
        }

        private static String composeCDataNodes(XElement node)
        {
            StringBuilder sb = new StringBuilder();
            foreach (XNode child in node.Nodes())
            {
                if (child.NodeType == XmlNodeType.CDATA)
                    sb.Append(((XCData)child).Value);
            }
            return sb.ToString().Trim(SPACES);
        }

        private static XObject selectStep(XObject node, String step, bool create, Dictionary<String, Object> parameters)
        {
            if (step.Length == 0)               // ""
                return selectOwnerDocument(node);
            if (".".Equals(step))               // "."
                return node;
            if ("..".Equals(step))              // ".."
                return node.Parent;
            if ("text()".Equals(step))          // "text()"
                return selectTextNode(node, create);
            if ("cdata()".Equals(step))          // "text()"
                return selectCDataNode(node, create);
            if (step.StartsWith("@"))           // "@attribute"
                return selectAttributeNode(node, step, create);
            if (step.IndexOf("[@") != -1)       // "n[@a='v'] | n[@a="v"] | n[@a={p}]
                return selectConditionNode(node, step, create, parameters);
            if (step.IndexOf("[") != -1)        // "n[i]" | n[{p}]
                return selectIndexedNode(node, step, create, parameters);
            else                                // n
                return selectNamedNode(node, step, create);
        }

        private static XDocument selectOwnerDocument(XObject node)
        {
            return node.NodeType == XmlNodeType.Document ? (XDocument)node : node.Document;
        }

        private static XObject selectTextNode(XObject node, bool create)
        {
            if (!create)
                return node;

            removeTextNodes((XElement)node);
            return createTextNode((XElement)node);
        }

        private static XObject selectCDataNode(XObject node, bool create)
        {
            if (!create)
                return node;

            removeCDataNodes((XElement)node);
            return createCDataNode((XElement)node);
        }


        private static XObject selectAttributeNode(XObject node, String step, bool create)
        {
            String name = step.Substring(1);
            XElement element = (XElement)node;
            foreach (XAttribute attribute in element.Attributes())
            {
                if (name.Equals(attribute.Name.LocalName))
                    return attribute;
            }

            if (!create)
                return null;

            return createAttributeNode(element, name);
        }

        private static XObject selectConditionNode(XObject node, String step, bool create, Dictionary<String, Object> parameters)
        {
            String nodeName, attribute, value;

            // name[@attribute='value']
            // name[@attribute="value"]
            // name[@attribute={param}]
            // name[@attribute=value]
            {
                int pos = step.IndexOf('[');
                nodeName = step.Substring(0, pos);

                String condition = step.Substring(pos + 1, step.Length - pos - 2);
                int peq = condition.IndexOf('=');

                attribute = condition.Substring(1, peq - 1).Trim();

                String quoted = condition.Substring(peq + 1).Trim();
                value = resolveValue(quoted, parameters);
            }

            foreach (XObject child in ((XElement)node).Nodes())
            {
                if (child.NodeType != XmlNodeType.Element)
                    continue;

                if (!nodeName.Equals(((XElement)child).Name.LocalName))
                    continue;

                if (((XElement)child).Attribute(attribute) == null)
                    continue;

                if (((XElement)child).Attribute(attribute).Equals(value))
                    return child;
            }

            if (!create)
                return null;

            {
                XElement newChild = new XElement(nodeName);
                newChild.Add(new XAttribute(attribute, value));
                ((XElement)node).Add(newChild);
                return newChild;
            }
        }

        private static XObject selectIndexedNode(XObject node, String step, bool create, Dictionary<String, Object> parameters)
        {
            int pos = step.IndexOf('[');
            String nodeName = step.Substring(0, pos);
            String index = step.Substring(pos + 1, step.Length - pos - 2);
            int at = resolveIndex(index, parameters);

            foreach (XNode child in ((XElement)node).Nodes())
            {
                if (nodeName.Equals(((XElement)child).Name.LocalName))
                {
                    if (at == 1)
                        return child;

                    --at;
                }
            }

            if (!create)
                return null;

            // deve creare abbastanza child 
            XElement newChild = null;
            while (at > 0)
            {
                newChild = new XElement(nodeName);
                ((XElement)node).Add(newChild);
                --at;
            }

            return newChild;
        }

        private static XObject selectNamedNode(XObject node, String nodeName, bool create)
        {
            foreach (XNode child in ((XElement)node).Nodes())
            {
                if (nodeName.Equals(((XElement)child).Name.LocalName))
                    return child;
            }

            if (!create)
                return null;

            XElement newChild = new XElement(nodeName);
            ((XElement)node).Add(newChild);
            return newChild;
        }


        private static XAttribute createAttributeNode(XElement element, String name)
        {
            XAttribute newAttribute = new XAttribute(name, "");
            element.Add(newAttribute);
            return newAttribute;
        }

        #endregion
    }

}
